﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace tomssoftware.Html
{
    /// <summary>
    /// Parses a stream for html content.
    /// This is a finite state machine which implements a lexical analyzer.
    /// </summary>
    public class HtmlParser : IDisposable
    {
        private Stream stream;
        private TextReader reader;

        public HtmlParser(Stream stream)
            : base()
        {
            if (stream == null) throw new ArgumentNullException("stream");
            this.stream = stream;
            this.reader = new StreamReader(stream);
        }

        public void Dispose()
        {
            if (reader != null) reader.Dispose();
            if (stream != null) stream.Dispose();
            reader = null;
            stream = null;
            GC.SuppressFinalize(this);
        }

        ~HtmlParser()
        {
            Dispose();
        }

        public ICollection<HtmlAttribute> Attributes
        {
            get { return GetAttributes(); }
        }

        /// <summary>
        /// Gets the hierarchichal level of the current location.
        /// </summary>
        public int Level { get; private set; }

        /// <summary>
        /// Gets the raw content of a value or an element.
        /// In case of an element, this would be the element name including all attributes.
        /// </summary>
        public string Content { get; private set; }

        /// <summary>
        /// Gets the type of the current token.
        /// </summary>
        public HtmlType Type { get; private set; }

        public string ElementName { get; private set; }
        public ElementId ElementId { get; private set; }

        public bool IsElement
        {
            get
            {
                return Type == HtmlType.OpenElement || Type == HtmlType.Element;
            }
        }

        private string GetElementName()
        {
            string s = Content.Trim();
            StringBuilder sb = new StringBuilder();
            bool reading = false;
            foreach (char c in s)
            {
                bool isElementChar = IsElementChar(c);
                if (!reading && isElementChar) reading = true;
                else if (reading && !isElementChar) break;
                if (reading) sb.Append(c);
            }
            return sb.ToString();
        }

        private bool IsElementChar(char c)
        {
            if (c >= '0' && c <= '9') return true;
            if (c >= 'a' && c <= 'z') return true;
            if (c >= 'A' && c <= 'Z') return true;
            return false;
        }

        /// <summary>
        /// Advances to the next html token.
        /// </summary>
        /// <returns>True if new token was read otherwise false to indicate eof.</returns>
        public bool Read()
        {
            char c;
            if (!ReadChar(out c)) return false;
            switch (c)
            {
                case '<': return ReadTagOrComment();
                default: return ReadContent(c);
            }
        }

        private bool ReadContent(char c)
        {

            ElementId = ElementId.unknown;
            ElementName = "";

            StringBuilder sb = new StringBuilder();
            sb.Append(c);
            char next;
            bool readNext = true;
            bool isInString = false;
            while (readNext && PeekChar(out next))
            {
                switch (next)
                {
                    //case '"':
                    //    isInString ^= true;
                    //    sb.Append(next);
                    //    ReadChar(out next);
                    //    break;

                    case '<': if (!isInString)
                        {
                            readNext = false;
                        }
                        else
                        {
                            sb.Append(next);
                            ReadChar(out next);
                        }
                        break;

                    default:
                        sb.Append(next);
                        ReadChar(out next);
                        break;
                }
            }

            Type = HtmlType.Content;
            Content = sb.ToString();
            return true;
        }

        private bool IsFormat(char c)
        {
            switch (c)
            {
                case '\n':
                case '\r': return true;
                default: return false;
            }
        }

        private bool ReadTagOrComment()
        {
            HtmlType type = HtmlType.OpenElement;
            char next;
            char lastchar = '<';
            bool ended = false;
            bool isInString = false;
            List<HtmlAttribute> attributes = new List<HtmlAttribute>();
            StringBuilder sb = new StringBuilder();
            sb.Append(lastchar);

            while (!ended && (ReadChar(out next)))
            {
                switch (next)
                {
                    case '>':
                        if (!isInString) ended = true;
                        break;

                    case '!':
                        if (!isInString && lastchar == '<') type = HtmlType.Comment;
                        break;

                    case '"':
                        isInString ^= true;
                        break;

                    case '/':
                        if (!isInString)
                        {
                            if (lastchar == '<') type = HtmlType.CloseElement;
                            else if (type != HtmlType.Comment) type = HtmlType.Element;
                        }
                        break;
                }
                lastchar = next;
                sb.Append(next);
            }
            this.Type = type;
            this.Content = sb.ToString();
            UpdateLevel(type);

            if (type == HtmlType.Comment)
            {
                ElementId = ElementId.unknown;
                ElementName = "";
            }
            else
            {
                ElementName = GetElementName();
                ElementId = HtmlMap.ToId(ElementName);
            }
            return true;
        }

        private void UpdateLevel(HtmlType type)
        {
            switch (type)
            {
                case HtmlType.OpenElement: Level++; break;
                case HtmlType.CloseElement: --Level; break;
            }
        }

        private bool IsWhiteSpace(char next)
        {
            return " \t\n\r".IndexOf(next) >= 0;
        }

        private bool PeekChar(out char c)
        {
            int v = reader.Peek();
            c = (char)v;
            return v >= 0;
        }

        private bool ReadChar(out char c)
        {
            int v = reader.Read();
            c = (char)v;
            return v >= 0;
        }

        public string ReadToEndElement()
        {
            if (Type == HtmlType.OpenElement)
            {
                ElementId id = ElementId;
                int level = 0;
                Read();
                StringBuilder sb = new StringBuilder(Content);
                while (Read())
                {
                    if (Type == HtmlType.OpenElement && ElementId == id) level++;
                    else if (Type == HtmlType.CloseElement && ElementId == id)
                    {
                        if (level == 0) break;
                        level--;
                    }
                    sb.Append(Content);
                }
                return sb.ToString();
            }
            else
            {
                return Content;
            }
        }

        public void SkipToEndElement()
        {
            if (Type == HtmlType.OpenElement)
            {
                ElementId id = ElementId;
                int level = 0;
                Read();
                while (Read())
                {
                    if (Type == HtmlType.OpenElement && ElementId == id) level++;
                    else if (Type == HtmlType.CloseElement && ElementId == id)
                    {
                        if (level == 0) break;
                        level--;
                    }
                }
            }
        }

        private static readonly HtmlAttribute[] EmptyAttributes = new HtmlAttribute[] { };

        private enum contentType
        {
            Unknown,
            Equals,
            Ignore,
            AttributeName,
            AttributeValue
        }

        private ICollection<HtmlAttribute> GetAttributes()
        {
            if (!IsElement) return EmptyAttributes;
            List<HtmlAttribute> attributes = new List<HtmlAttribute>();
            contentType ct = contentType.Unknown;
            StringBuilder sb = new StringBuilder();
            string attributeName = string.Empty;
            bool isQuoted = false;

            foreach (char c in Content)
            {
                switch (c)
                {
                    case '"': isQuoted ^= true; break;
                }
                switch (ct)
                {
                    case contentType.Unknown:
                        if (IsElementChar(c)) ct = contentType.Ignore;
                        break;

                    case contentType.Ignore:
                        if (!isQuoted && IsWhiteSpace(c)) ct = contentType.AttributeName;
                        break;

                    case contentType.AttributeName:
                        if (c == '=' && !isQuoted)
                        {
                            attributeName = sb.ToString().Trim();
                            sb.Length = 0;
                            sb = new StringBuilder();
                            ct = contentType.Equals;
                        }
                        break;

                    case contentType.Equals:
                        ct = contentType.AttributeValue;
                        break;

                    case contentType.AttributeValue:
                        if (!isQuoted && IsEndCharOrWhiteSpace(c))
                        {
                            string value = sb.ToString();
                            HtmlAttribute a = new HtmlAttribute(attributeName, value);
                            attributes.Add(a);
                            sb.Length = 0;
                            sb = new StringBuilder();
                            ct = contentType.AttributeName;
                        }
                        break;
                }
                if (ct == contentType.AttributeName || ct == contentType.AttributeValue)
                {
                    sb.Append(c);
                }
            }

            return attributes;
        }

        private bool IsEndCharOrWhiteSpace(char c)
        {
            if (IsWhiteSpace(c)) return true;
            return c == '/' || c == '>';
        }
    }
}
